4.5 延迟调用

语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作

func main() { 
   f,err:=os.Open("./main.go") 
   if err!=nil{ 
       log.Fatalln(err) 
    } 
  
   defer f.Close()          // 仅注册,直到main退出前才执行 
  
    ...do something... 
}

注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包。

func main() { 
   x,y:=1,2
  
   defer func(a int) { 
       println("defer x,y= ",a,y)   //y为闭包引用 
    }(x)                // 注册时复制调用参数 
  
   x+=100               // 对x的修改不会影响延迟调用 
   y+=200                       
   println(x,y) 
}

输出:

101 202
defer x,y=1 202

延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。

多个延迟注册按FILO次序执行

func main() { 
   defer println("a") 
   defer println("b") 
}

输出:

b
a

编译器通过插入额外指令来实现延迟调用执行,而return和panic语句都会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,它会先更新返回值。

func test() (z int) { 
   defer func() { 
       println("defer:",z) 
       z+=100         // 修改命名返回值 
    }() 
  
   return 100          // 实际执行次序:z=100,call defer,ret
} 
  
func main() { 
   println("test:",test()) 
}
 

输出:

defer:100
test:200

有关defer更详细的分析,请阅读下卷《源码剖析》。

误用

千万记住,延迟调用在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。

案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。

func main() { 
   for i:=0;i<10000;i++ { 
       path:=fmt.Sprintf("./log/%d.txt",i) 
  
       f,err:=os.Open(path) 
       if err!=nil{ 
           log.Println(err) 
           continue
        } 
  
        // 这个关闭操作在main函数结束时才会执行,而不是当前循环中执行 
        // 这无端延长了逻辑结束时间和f的生命周期,平白多消耗了内存等资源 
       defer f.Close() 
  
        ...do something... 
    } 
}
 

应该直接调用,或重构为函数,将循环和处理算法分离。

func main() { 
// 日志处理算法 
   do:=func(n int) { 
       path:=fmt.Sprintf("./log/%d.txt",n) 
  
       f,err:=os.Open(path) 
       if err!=nil{ 
           log.Println(err) 
           continue
        } 
  
        // 该延迟调用在此匿名函数结束时执行,而非main
       defer f.Close() 
  
        ...do something... 
    } 
  
   for i:=0;i<10000;i++ { 
       do(i) 
    } 
}
 

性能

相比直接用CALL汇编指令调用函数,延迟调用则须花费更大代价。这其中包括注册、调用等操作,还有额外的缓存开销。

以最常用的mutex为例,我们简单对比一下两者的性能差异。

var m sync.Mutex
  
func call() { 
   m.Lock() 
   m.Unlock() 
} 
  
func deferCall() { 
   m.Lock() 
   defer m.Unlock() 
} 
  
func BenchmarkCall(b*testing.B) { 
   for i:=0;i<b.N;i++ { 
       call() 
    } 
} 
  
func BenchmarkDefer(b*testing.B) { 
   for i:=0;i<b.N;i++ { 
       deferCall() 
    } 
}
 

输出:

BenchmarkCall-4   100000000         22.4 ns/op
BenchmarkDefer-4  20000000          93.8 ns/op

相差几倍的结果足以引起重视。尤其是那些性能要求高且压力大的算法,应避免使用延迟调用。